/*
 * Copyright 2009-2010 Nanjing RedOrange ltd (http://www.red-orange.cn)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package redora.util;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import redora.api.fetch.Page;
import redora.exceptions.JSONException;
import redora.exceptions.PagingException;
import redora.service.BusinessRuleViolation;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Date;
import java.util.Set;
import java.util.logging.Logger;

import static java.sql.Types.*;
import static java.util.logging.Level.INFO;
import static redora.db.SQLParameter.yyyyMMdd;
import static redora.db.SQLParameter.yyyyMMddHHMMSS;

/**
 * Some common methods for JSON used in projects based on Redora. The writeXXX
 * methods write JSON speak to an output stream.
 * 
 * @author Nanjing RedOrange (www.red-orange.cn)
 * 
 */
public class JSONWriter {

    static final transient Logger l = Logger.getLogger("redora.util.JSONWriter");

    final PrintWriter out;

    /**
     * Possible statuses. Mind you in JSON the ordinal vale is used. So, when changing
     * this also update redora.client.validation.BusinessRuleCheckHandler#ResponseStatus
     */
    public enum ResponseStatus {
        success, brViolated, expired, refused
    }

    /**
     * Initialized by setting the output writer and setting the content-type
     * to 'application/json'.
     * @param resp (Mandatory)
     * @throws IOException Passing on exception from getting the output writer
     */
    public JSONWriter(@NotNull HttpServletResponse resp) throws IOException {
        this(resp.getWriter());
        resp.setContentType("application/json");
    }

    /**
     * Creates a JSONWriter for usage outside a servlet container. Normally
     * you only use this for testing or when you want to stream the output to disk.
     * @param out (Mandatory)
     */
    public JSONWriter(@NotNull PrintWriter out) {
        this.out = out;
    }

    /** prints "null" */
    public void nullOut() {
        out.print("null");
    }

    /**
     * Prints JSON key like 'key:'
     * @param key (Mandatory)
     */
    public void key(@NotNull String key) {
        out.print(key);
        out.print(':');
    }

    /**
     * Prints JSON key like '"key":'
     * @param key (Mandatory)
     */
    public void keyQuoted(@NotNull String key) {
        out.print('\"');
        out.print(key);
        out.print('\"');
        out.print(':');
    }

    /**
     * Writes 'normal' values from resultset to printwriter in a json style
     * markup like key:value. It will quote the value according the sqlType.
     * 
     * @param sqlType   java.sql.Types
     * @param key       (Mandatory)
     * @param value     (Optional)
     * @throws JSONException When escaping the characters fails
     */
    public void rs(int sqlType, @NotNull String key, @Nullable String value) throws JSONException {
        key(key);
        if (value == null) {
            nullOut();
        } else if (sqlType == BIGINT || sqlType == INTEGER || sqlType == DOUBLE) {
            out.print(value);
        } else {
            out.print('\"');
            if (sqlType == CHAR) {
                out.print(value);
            } else {
                new JSONTokener(value).stream(out);
            }
            out.print('\"');
        }
    }

    /**
     * Prints escaped and quoted like "'key':'value'"
     * @param key (Mandatory)
     * @param value (Mandatory)
     * @throws JSONException Exceptions when escaping the value
     */
    public void escaped(@NotNull String key, @Nullable String value) throws JSONException {
        key(key);
        if (value == null) {
            nullOut();
        } else {
            out.print('\"');
            new JSONTokener(value).stream(out);
            out.print('\"');
        }
    }

    public void quoteValue(@Nullable Object value) {
        if (value == null) {
            nullOut();
        } else {
            out.print('\"');
            out.print(value);
            out.print('\"');
        }
    }

    public void quoted(@NotNull String key, @Nullable Object value) {
        key(key);
        quoteValue(value);
    }

    public void kvp(@NotNull String key, @Nullable Object value) {
        key(key);
        if (value == null)
            nullOut();
        else
            out.print(value);
    }

    /**
     * Writes 'normal' values from resultset to printwriter in a json style
     * markup
     * 
     * @param key
     * @param in
     * @param wasNull
     */
    public void stream(@NotNull String key, InputStream in, boolean wasNull)
            throws JSONException {
        key(key);
        if (wasNull) {
            nullOut();
        } else {
            out.print('\"');
            try {
                new JSONTokener(new InputStreamReader(in, "UTF-8")).stream(out);
            } catch (UnsupportedEncodingException ex) {
                throw new JSONException(ex);
            }
            out.print('\"');
        }
    }

    public void date(@NotNull String key, java.sql.Date date, boolean wasNull) {
        if (wasNull && date == null) {
            key(key);
            nullOut();
        } else {
            date(key, new Date(date.getTime()));
        }
    }

    public void date(@NotNull String key, @Nullable Date date) {
        key(key);
        if (date == null) {
            nullOut();
        } else {
            out.print('\"');
            out.print(yyyyMMdd.format(date));
            out.print('\"');
        }
    }

    public void dateTime(@NotNull String key, Date date, boolean wasNull) {
        if (wasNull) {
            key(key);
            nullOut();
        } else {
            dateTime(key, new Date(date.getTime()));
        }
    }

    public void dateTime(@NotNull String key, @Nullable Date date) {
        key(key);
        if (date == null) {
            nullOut();
        } else {
            out.print('\"');
            out.print(yyyyMMddHHMMSS.format(date));
            out.print('\"');
        }
    }

    public void start(int status) {
        out.print("{response:{status:");
        out.print(String.valueOf(status));
    }

    public void startSuccess() {
        start(ResponseStatus.success.ordinal());
    }

    public void responseStart() {
        startSuccess();
        out.print(",data:");
    }

    public void responseEnd() {
        out.print("}}");
    }

    public void responseSuccess() {
        startSuccess();
        out.print("}}");
    }

    /**
     * The client is not logged in. The expired message is returned when the client needs to be logged in.
     */
     public void expired() {
        start(ResponseStatus.expired.ordinal());
        responseEnd();
    }

    /**
     * The client is logged in but it is trying to do something it is not allowed.
     */
    public void refused() {
       start(ResponseStatus.refused.ordinal());
       responseEnd();
   }

    public void responseWithPageStart(@NotNull Page page) throws PagingException {
        startSuccess();
        out.print(",page:{");
        out.print(page.toJSON());
        out.print("},data:[");
    }

    public void responseWithArrayStart() {
        startSuccess();
        out.print(",data:[");
    }

    public void responseWithArrayEnd() {
        out.print("]}}");
    }

    public void persist(@NotNull Set<BusinessRuleViolation> ret) {
        if (ret.isEmpty()) {
            responseSuccess();
        } else {
            l.log(INFO, "Responding {0} business rule violations", ret.size());
            violation(ret);
        }
    }

    public void violation(@NotNull Set<BusinessRuleViolation> ret) {
        start(ResponseStatus.brViolated.ordinal());
        out.print(",violations:[");
        char comma = ' ';
        for (BusinessRuleViolation br : ret) {
            out.print(comma);
            out.print(br.toJSON());
            comma = ',';
        }
        out.print("]}}");
    }

    @Deprecated
    public void customResponse(int status) {
        start(status);
        responseEnd();
    }

    public void print(char c) {
        out.print(c);
    }

    @Deprecated //Create methods for this
    public void print(String s) {
        out.print(s);
    }

    public void flush() {
        out.flush();
    }
}
